/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baidu.jprotobuf.rpc.client;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.zip.GZIPInputStream;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.util.StringUtils;

import com.baidu.bjf.remoting.protobuf.IDLProxyObject;
import com.baidu.jprotobuf.rpc.support.IOUtils;

/**
 * SimpleHttpRequestExecutor implementation that uses standard J2SE facilities
 * to execute POST requests, without support for HTTP authentication or
 * advanced configuration options.
 * 
 * @author xiemalin
 * @since 1.0.0
 */
public class SimpleHttpRequestExecutor extends AbstractHttpRequestExecutor {
    
    
    /**
     * Execute the given request through a standard J2SE HttpURLConnection.
     * <p>This method implements the basic processing workflow:
     * The actual work happens in this class's template methods.
     * 
     * @see #openConnection
     * @see #prepareConnection
     * @see #writeRequestBody
     * @see #validateResponse
     * @see #readResponseBody
     * 
     * @param invoker
     */
    public IDLProxyObject doExecuteRequest(IDLHttpClientInvoker invoker, IDLProxyObject input,
            IDLProxyObject output) throws IOException {
        HttpURLConnection con = openConnection(invoker);
        
        int contentLength = 0;
        byte[] bb = null;
        if (input != null) {
            bb = input.encode();
            if (bb != null) {
                contentLength = bb.length;
            }
        }
        
        prepareConnection(con, contentLength);
        if (bb != null) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream(contentLength);
            bos.write(bb);
            writeRequestBody(con, bos);
        }
        
        
        validateResponse(con);
        
        if (output != null) {
            
            InputStream responseBody = readResponseBody(con);
            
            byte[] byteArray = IOUtils.toByteArray(responseBody);
            
            IDLProxyObject idlProxyObject = output.decode(byteArray);
            return idlProxyObject;
        }
        
        return null;
    }

    /**
     * Open an HttpURLConnection for the given remote invocation request.
     * @param config the HTTP invoker configuration that specifies the
     * target service
     * @return the HttpURLConnection for the given request
     * @throws IOException if thrown by I/O methods
     * @see java.net.URL#openConnection()
     */
    protected HttpURLConnection openConnection(IDLHttpClientInvoker invoker) throws IOException {
        URLConnection con = new URL(invoker.getServiceUrl()).openConnection();
        if (!(con instanceof HttpURLConnection)) {
            throw new IOException("Service URL [" + invoker.getServiceUrl() + "] is not an HTTP URL");
        }
        
        if (invoker.getConnectTimeout() > 0) {
            con.setConnectTimeout(invoker.getConnectTimeout());
        }
        
        if (invoker.getReadTimeout() > 0) {
            con.setReadTimeout(invoker.getReadTimeout());
        }
        
        return (HttpURLConnection) con;
    }
    
    /**
     * Prepare the given HTTP connection.
     * <p>The default implementation specifies POST as method,
     * "application/x-java-serialized-object" as "Content-Type" header,
     * and the given content length as "Content-Length" header.
     * @param con the HTTP connection to prepare
     * @param contentLength the length of the content to send
     * @throws IOException if thrown by HttpURLConnection methods
     * @see java.net.HttpURLConnection#setRequestMethod
     * @see java.net.HttpURLConnection#setRequestProperty
     */
    protected void prepareConnection(HttpURLConnection con, int contentLength) throws IOException {
        con.setDoOutput(true);
        con.setRequestMethod(HTTP_METHOD_POST);
        con.setRequestProperty(HTTP_HEADER_CONTENT_TYPE, getContentType());
        con.setRequestProperty(HTTP_HEADER_CONTENT_LENGTH, Integer.toString(contentLength));
        LocaleContext locale = LocaleContextHolder.getLocaleContext();
        if (locale != null) {
            con.setRequestProperty(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale.getLocale()));
        }
        if (isAcceptGzipEncoding()) {
            con.setRequestProperty(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
        }
    }
    
    /**
     * Determine whether the given response is a GZIP response.
     * <p>Default implementation checks whether the HTTP "Content-Encoding"
     * header contains "gzip" (in any casing).
     * @param con the HttpURLConnection to check
     */
    protected boolean isGzipResponse(HttpURLConnection con) {
        String encodingHeader = con.getHeaderField(HTTP_HEADER_CONTENT_ENCODING);
        return (encodingHeader != null && encodingHeader.toLowerCase().indexOf(ENCODING_GZIP) != -1);
    }
    
    /**
     * Validate the given response as contained in the HttpURLConnection object,
     * throwing an exception if it does not correspond to a successful HTTP response.
     * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
     * parsing the response body and trying to deserialize from a corrupted stream.
     * @param con the HttpURLConnection to validate
     * @throws IOException if validation failed
     * @see java.net.HttpURLConnection#getResponseCode()
     */
    protected void validateResponse(HttpURLConnection con)
            throws IOException {

        if (con.getResponseCode() >= 300) {
            throw new IOException(
                    "Did not receive successful HTTP response: status code = " + con.getResponseCode() +
                    ", status message = [" + con.getResponseMessage() + "]");
        }
    }
    
    /**
     * Set the given serialized remote invocation as request body.
     * <p>The default implementation simply write the serialized invocation to the
     * HttpURLConnection's OutputStream. This can be overridden, for example, to write
     * a specific encoding and potentially set appropriate HTTP request headers.
     * @param con the HttpURLConnection to write the request body to
     * @param baos the ByteArrayOutputStream that contains the serialized
     * RemoteInvocation object
     * @throws IOException if thrown by I/O methods
     * @see java.net.HttpURLConnection#getOutputStream()
     * @see java.net.HttpURLConnection#setRequestProperty
     */
    protected void writeRequestBody(HttpURLConnection con, ByteArrayOutputStream baos)
            throws IOException {

        baos.writeTo(con.getOutputStream());
    }
    
    
    /**
     * Extract the response body from the given executed remote invocation
     * request.
     * <p>The default implementation simply reads the serialized invocation
     * from the HttpURLConnection's InputStream. If the response is recognized
     * as GZIP response, the InputStream will get wrapped in a GZIPInputStream.
     * @param con the HttpURLConnection to read the response body from
     * @return an InputStream for the response body
     * @throws IOException if thrown by I/O methods
     * @see #isGzipResponse
     * @see java.util.zip.GZIPInputStream
     * @see java.net.HttpURLConnection#getInputStream()
     * @see java.net.HttpURLConnection#getHeaderField(int)
     * @see java.net.HttpURLConnection#getHeaderFieldKey(int)
     */
    protected InputStream readResponseBody(HttpURLConnection con)
            throws IOException {

        if (isGzipResponse(con)) {
            // GZIP response found - need to unzip.
            return new GZIPInputStream(con.getInputStream());
        }
        else {
            // Plain response found.
            return con.getInputStream();
        }
    }
    
    
}
